package com.mycms.architecture.jsf.component;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;

import javax.faces.context.FacesContext;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>
 * Componente Seam che, dato il nome relativo di un'oggetto html (tipicamente un'immagine
 * o un foglio di stile) cerca se esiste una versione specializzata per la combinazione
 * corrente di domini (ad esempio per istituto, canale, ecc.) e di lingua, poi via via
 * tenta le versioni pi� generali.
 * {@link cedacri.arch.jsf.component.HtmlCustomGraphicImage} e {@link HtmlStyleSheet} si
 * basano su questa funzionalit�.<br>
 * Gli url vengono memorizzati in una cache (a livello di applicazione) per ottimizzare le
 * prestazioni.
 * </p>
 * <p>
 * Il formato del percorso cercato � il seguente:
 * </p>
 * <code><i>percorsoBase</i>/<b>codiceDominio1</b>.<b>valoreDominio1</b>-<b>codiceDominio2</b>.<b>
 * valoreDominio2</b>-.../<i>lang</i>.<b>lingua</b></code>
 * <p>
 * Viene per prima cercato un url con tutti i domini disponibili e la lingua. Poi via via
 * vengono provate tutte le altre combinazioni. Infine viene cercato il nome base. Come si
 * pu� notare dal formato, le varianti per la lingua sono contenute in directories
 * relative alla personalizzazione per dominio (mentre queste ultime sono tutte nel primo
 * livello, "piatte").
 * </p>
 * <p>
 * Esempio: supponiamo che domini abbiano codici "ist", "prod" e "chan" (con priorit�
 * decrescente 2, 1, 0), che valgano rispettivamente "06930", "LIGHT", "MOBILE", che il
 * percorso base sia "/images", che la lingua dell'utente sia "it" e che il l'url relativo
 * cercato sia "logo.jpg". I percorsi cercati saranno:
 * </p>
 * <ul>
 * <li>/images/ist.06930-prod.LIGHT-chan.MOBILE/lang.it/logo.jpg</li>
 * <li>/images/ist.06930-prod.LIGHT-chan.MOBILE/logo.jpg</li>
 * <li>/images/ist.06930-prod.LIGHT/lang.it/logo.jpg</li>
 * <li>/images/ist.06930-prod.LIGHT/logo.jpg</li>
 * <li>/images/ist.06930-chan.MOBILE/lang.it/logo.jpg</li>
 * <li>/images/ist.06930-chan.MOBILE/logo.jpg</li>
 * <li>/images/ist.06930/lang.it/logo.jpg</li>
 * <li>/images/ist.06930/logo.jpg</li>
 * <li>/images/prod.LIGHT-chan.MOBILE/lang.it/logo.jpg</li>
 * <li>/images/prod.LIGHT-chan.MOBILE/logo.jpg</li>
 * <li>/images/prod.LIGHT/lang.it/logo.jpg</li>
 * <li>/images/prod.LIGHT/logo.jpg</li>
 * <li>/images/chan.MOBILE/lang.it/logo.jpg</li>
 * <li>/images/chan.MOBILE/logo.jpg</li>
 * <li>/images/lang.it/logo.jpg</li>
 * <li>/images/logo.jpg</li>
 * </ul>
 * <p>
 * E' possibile una seconda modalit� di funzionamento che, invece di rendere un singolo
 * url, rende la lista di tutti quelli per cui esiste l'oggetto cercato (in ordine dal pi�
 * dettagliato al pi� generico).
 * </p>
 */
@Name(UrlResolver.COMPONENT_NAME)
@Scope(ScopeType.APPLICATION)
@BypassInterceptors
@Install(precedence = Install.FRAMEWORK)
public class UrlResolver
{
	public static final String COMPONENT_NAME = "mycms.architecture.urlResolver";

	private Logger tracer = LoggerFactory.getLogger(getClass());

	/**
	 * Cache per la modalit� singola, con in chiave la combinazione di url, domini e
	 * lingua ed in valore l'url precalcolato e pronto per l'uso in un link.
	 */
	private final ConcurrentHashMap<ChiaveCache, String> cacheSingoliUrl = new ConcurrentHashMap<ChiaveCache, String>();

	/**
	 * Cache per la modalit� multipla, con in chiave la combinazione di url, domini e
	 * lingua ed in valore una lista di url precalcolati e pronti per l'uso in una serie
	 * di link.
	 */
	private final ConcurrentHashMap<ChiaveCache, List<String>> cacheListeUrl = new ConcurrentHashMap<ChiaveCache, List<String>>();

	/**
	 * Prova tutte le combinazioni di url per i domini e la lingua. Rende un url singolo
	 * gi� codificato per l'uso in un attributo "src".
	 * 
	 * @param base url base (se lo si vuole a partire dalla root dell'applicazione,
	 *            aggiungere un "/" davanti).
	 * @param name percorso relativo dell'elemento (rispetto a base).
	 * @param context facescontext utilizzato per risolvere il percorso e renderlo
	 *            assoluto.
	 * @return url gi� pronto ed escapizzato per essere utilizzato in un attributo src o
	 *         simile; null se non viene trovato alcun elemento.
	 */
	public String resolveSingleUrl(String base, String name, FacesContext context)
	{
		/*Profile profilo = (Profile) Component.getInstance(Profile.COMPONENT_NAME);*/
		Locale seamLocale = org.jboss.seam.core.Locale.instance();
		String lingua = seamLocale.getLanguage();
		return resolveSingleUrl(base, name, context, lingua);
	}

	/**
	 * Prova tutte le combinazioni di url per i domini e la lingua. Rende un url singolo
	 * gi� codificato per l'uso in un attributo "src".
	 * 
	 * @param base url base (se lo si vuole a partire dalla root dell'applicazione,
	 *            aggiungere un "/" davanti).
	 * @param name percorso relativo dell'elemento (rispetto a base).
	 * @param context facescontext utilizzato per risolvere il percorso e renderlo
	 *            assoluto.
	 * @param lingua lingua da usare nella ricerca.
	 * @param domini valorei dei domini di personalizzazione da usare nella ricerca.
	 * @return url gi� pronto ed escapizzato per essere utilizzato in un attributo src o
	 *         simile; null se non viene trovato alcun elemento.
	 */
	public String resolveSingleUrl(String base, String name, FacesContext context,
			String lingua)
	{

		String urlPerLink = provaUrl(base, name, context);
		if (urlPerLink != null)
		{
			tracer.trace("{} aggiunto alla cache dei singoli url",
					urlPerLink);
			return urlPerLink;
		}
		else {
			return null;
		}
	}
	
	/**
	 * Prova tutte le combinazioni di url per i domini e la lingua. Rende una lista di
	 * urls gi� codificati per l'uso in attributi "src".
	 * 
	 * @param base url base (se lo si vuole a partire dalla root dell'applicazione,
	 *            aggiungere un "/" davanti).
	 * @param name percorso relativo dell'elemento (rispetto a base).
	 * @param genericFirst se true la lista � in ordine dal pi� generico al pi�
	 *            dettagliato, se false dal pi� dettagliato al pi� generico. ATTENZIONE: �
	 *            importante che l'applicazione mantegna la stessa politica in modo
	 *            coerente, perch� questo campo viene utilizzato solo la prima volta, poi
	 *            quando l'url � in cache viene ignorato nelle chiamate successive.
	 * @param context facescontext utilizzato per risolvere il percorso e renderlo
	 *            assoluto.
	 * @return lista di url gi� pronti ed escapizzati per essere utilizzati in una serie
	 *         di attributi src o simili; lista vuota se non viene trovato alcun elemento.
	 */
	public List<String> resolveMultipleUrls(String base, String name,
			boolean genericFirst, FacesContext context)
	{
		/*Profile profilo = (Profile) Component.getInstance(Profile.COMPONENT_NAME);*/
		Locale seamLocale = org.jboss.seam.core.Locale.instance();
		String lingua = seamLocale.getLanguage();
		return resolveMultipleUrls(base, name, genericFirst, context, lingua);
	}

	/**
	 * Prova tutte le combinazioni di url per i domini e la lingua. Rende un una lista di
	 * url gi� codificati per l'uso in attributi "src".
	 * 
	 * @param base url base (se lo si vuole a partire dalla root dell'applicazione,
	 *            aggiungere un "/" davanti).
	 * @param name percorso relativo dell'elemento (rispetto a base).
	 * @param genericFirst se true la lista � in ordine dal pi� generico al pi�
	 *            dettagliato, se false dal pi� dettagliato al pi� generico. ATTENZIONE: �
	 *            importante che l'applicazione mantegna la stessa politica in modo
	 *            coerente, perch� questo campo viene utilizzato solo la prima volta, poi
	 *            quando l'url � in cache viene ignorato nelle chiamate successive.
	 * @param context facescontext utilizzato per risolvere il percorso e renderlo
	 *            assoluto.
	 * @param lingua lingua da usare nella ricerca.
	 * @param domini valorei dei domini di personalizzazione da usare nella ricerca.
	 * @return lista di url gi� pronti ed escapizzati per essere utilizzati in una serie
	 *         di attributi src o simili; lista vuota se non viene trovato alcun elemento.
	 */
	public List<String> resolveMultipleUrls(String base, String name,
			boolean genericFirst, FacesContext context, String lingua)
	{
		List<String> l = new ArrayList<String>();
		String urlPerLink = provaUrl(base, name, context);
		l.add(urlPerLink);
		return l;
	}

	/**
	 * Calcola un url a partire da base, nome e parte di domini/lingua, poi ne verifica
	 * l'esistenza nella webroot. Se esiste rende l'url stesso espanso per includere il
	 * contesto dell'applicazione e codificato per l'output (insomma adatto ad essere
	 * inserito direttamente in un link). Se non lo trova rende null.
	 * 
	 * @param base percorso base.
	 * @param name nome semplice dell'elemento.
	 * @param combinazioneDominiLingua parte del percorso relativa a domini e lingua.
	 * @param context facestcontext per effettuare l'encoding.
	 * @return url espanso ed escapizzato, oppure null se non trovato.
	 */
	private String provaUrl(String base, String name,
			FacesContext context)
	{
		StringBuilder b = new StringBuilder();
		b.append(base);
		b.append('/');
		b.append(name);
		String nomeCompletoCandidato = b.toString();
		return getEncodedUrlIfExists(context, nomeCompletoCandidato);
	}

	/**
	 * Verifica l'esistenza nella webroot dell'url specificato. Se esiste rende l'url
	 * stesso espanso per includere il contesto dell'applicazione e codificato per
	 * l'output (insomma adatto ad essere inserito direttamente in un link). Se non lo
	 * trova rende null.
	 * 
	 * @param context facescontext.
	 * @param url url da verificare.
	 * @return url utilizzabile per un link, oppure null se oggetto non trovato in
	 *         webroot.
	 */
	private String getEncodedUrlIfExists(FacesContext context, String url)
	{
		URL urlTrovato;
		try
		{
			urlTrovato = context.getExternalContext().getResource(url);
		}
		catch (MalformedURLException mue)
		{
			throw new RuntimeException(mue);
		}

		if (urlTrovato != null)
		{
			String contextualizedImageUrl = context.getApplication().getViewHandler().getResourceURL(
					context, url);
			String encodedUrl = context.getExternalContext().encodeResourceURL(
					contextualizedImageUrl);
			return encodedUrl;
		}
		else
		{
			return null;
		}
	}

	/** chiave per la mappa-cache di url */
	private static class ChiaveCache
	{
		/** percorso base dell'elemento */
		public String base;

		/** url relativo dell'elemento */
		public String nome;

		/** combinazione di domini */
		public String[] domini;

		/** lingua dell'utente */
		public String lingua;

		/**
		 * Costruttore.
		 * 
		 * @param base percorso base dell'elemento.
		 * @param nome url relativo dell'elemento.
		 * @param domini combinazione di domini.
		 * @param lingua lingua dell'utente.
		 */
		public ChiaveCache(String base, String nome, String[] domini, String lingua)
		{
			this.base = base;
			this.nome = nome;
			this.domini = domini;
			this.lingua = lingua;
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode()
		{
			final int prime = 31;
			int result = 1;
			result = prime * result + ((base == null) ? 0 : base.hashCode());
			result = prime * result + Arrays.hashCode(domini);
			result = prime * result + ((nome == null) ? 0 : nome.hashCode());
			result = prime * result + ((lingua == null) ? 0 : lingua.hashCode());
			return result;
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj)
		{
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ChiaveCache other = (ChiaveCache) obj;
			if (base == null)
			{
				if (other.base != null)
					return false;
			}
			else if ( !base.equals(other.base))
				return false;
			if ( !Arrays.equals(domini, other.domini))
				return false;
			if (nome == null)
			{
				if (other.nome != null)
					return false;
			}
			else if ( !nome.equals(other.nome))
				return false;
			if (lingua == null)
			{
				if (other.lingua != null)
					return false;
			}
			else if ( !lingua.equals(other.lingua))
				return false;
			return true;
		}

		/*
		 * (non-Javadoc)
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString()
		{
			return ToStringBuilder.reflectionToString(this);
		}

	}

	/**
	 * Svuota le cache.
	 */
	public void clearCache()
	{
		cacheListeUrl.clear();
		cacheSingoliUrl.clear();
	}
}
